Refactoring PostcodeApp - DI toepassen
Home

Refactoring PostcodeApp - DI toepassen

Refactoring PostcodeApp - DI toepassen

what-is-dependency-injection

We hebben drie TryOut methoden moeten maken voor elk van de data storage mogelijkheden (csv, xml en json).

We willen de code nu 'refactoren' en slechts één TryOut methode overhouden.

De techniek die we hiervoor gaan gebruiken is Dependency Injection (Inversion of Control en Dependency Injection in .NET Core).

Video

Stappenplan Dependency Injection

  1. Installeer het Microsoft.Extensions.DependencyInjection pakket via NuGet Package Console Zie Microsoft.Extensions.DependencyInjection op NuGet om te leren hoe je dat moet doen.
  2. Om te verifiëren dat het pakket geïnstalleerd is open je het PostCodeApp.csproj bestand door met de rechermuisknop te klikken op de projectnaam in de verkennen en Edit PostcodeApp.csproj te kiezen uit de lijst:
    Visual Studio Microsoft.Extensions.DependencyInjection installed
    Visual Studio Microsoft.Extensions.DependencyInjection installed
  3. De service-provider
    Als voorbeeld voor de registratie van een service gebruiken we de interface die we in Realisatiefase PostcodeApp gemaakt hebben.
    1. De naam van die service is IPostcode en die staat in het bestand /Dal/IPostcode.cs.
      namespace PostcodeApp.Dal
      {
          public interface IPostcode
          {
              // Property signatures:
              // Een Postcode BLL object om de opgehaalde waarden
              // in op te slagen
              Bll.Postcode Postcode { get; set; }
              // Error message
              string Message { get; set; }
              string ConnectionString { get; set; }
              public char Separator { get; set; }
      
              // method signatures
              bool Create();
              bool Create(char separator);
              bool ReadAll();
          }
      }
      
      
    2. De drie klassen om csv, json en xml bestanden in te lezen zijn van deze signatuur. Let erop dat alle eigenschappen en methoden van de interface in deze drie methoden geïmplementeerd zijn. Ik plaats hier nogmaals de broncode:
      1. PostcodeCsv
        using System;
        using System.Collections.Generic;
        using System.IO;
        
        namespace PostcodeApp.Dal
        {
            class PostcodeCsv : IPostcode
            {
                // Een Postcode BLL object om de opgehaalde waarden
                // in op te slagen
                public Bll.Postcode Postcode { get; set; }
                // Error message
                public string Message { get; set; }
                private string connectionString = @"Data/Postcode";
                public string ConnectionString
                {
                    get
                    {
                        return connectionString + ".csv";
                    }
                    set
                    {
                        connectionString = value;
                    }
                }
        
                public char Separator { get; set; } = ';';
        
                public PostcodeCsv(Bll.Postcode postcode)
                {
                    Postcode = postcode;
                }
        
                public bool ReadAll()
                {
                    Helpers.Tekstbestand bestand = new Helpers.Tekstbestand();
                    bestand.FileName = ConnectionString;
                    if (bestand.Lees())
                    {
                        string[] postcodes = bestand.Text.Split('\n');
                        try
                        {
                            List<Bll.Postcode> list = new List<Bll.Postcode>();
                            foreach (string s in postcodes)
                            {
                                if (s.Length > 0)
                                {
                                    list.Add(ToObject(s));
                                }
                            }
                            Postcode.List = list;
                            Message = $"Het bestand {ConnectionString} is gedesialiseerd!";
                            return true;
                        }
                        catch (Exception e)
                        {
                            // Melding aan de gebruiker dat iets verkeerd gelopen is.
                            // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                            Message = $"Bestand {ConnectionString} is niet gedeserialiseerd.\nFoutmelding {e.Message}.";
                            return false;
                        }
                    }
                    else
                    {
                        Message = $"Bestand {ConnectionString} is niet gedeserialiseerd.\nFoutmelding {bestand.Melding}.";
                        return false;
                    }
                }
        
                private Bll.Postcode ToObject(string line)
                {
                    Bll.Postcode postcode = new Bll.Postcode();
                    string[] values = line.Split(Separator);
                    postcode.Code = values[0];
                    postcode.Plaats = values[1];
                    postcode.Provincie = values[2];
                    postcode.Localite = values[3];
                    postcode.Province = values[4];
                    return postcode;
                }
        
                /// <summary>
                /// De Create van CRUD
                /// In het geval van een CSV bestand wordt de hele List gecreëerd.
                /// </summary>
                /// <returns></returns>
                public bool Create()
                {
                    try
                    {
                        TextWriter writer = new StreamWriter(ConnectionString);
                        foreach (Bll.Postcode item in Postcode.List)
                        {
                            // One of the most versatile and useful additions to the C# language in version 6
                            // is the null conditional operator ?.Post           
                            writer.WriteLine("{0}{5}{1}{5}{2}{5}{3}{5}{4}",
                                item?.Code,
                                item?.Plaats,
                                item?.Provincie,
                                item?.Localite,
                                item?.Province,
                                Separator);
                        }
                        writer.Close();
                        Message = $"Het bestand met de naam {ConnectionString} is gemaakt!";
                        return true;
                    }
                    catch (Exception e)
                    {
                        // Melding aan de gebruiker dat iets verkeerd gelopen is.
                        // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                        Message = $"Bestand met naam {ConnectionString} niet gemaakt.\nFoutmelding {e.Message}.";
                        return false;
                    }
                }
        
                // Een overload om tegelijkertijd de separator in te stellen
                public bool Create(char separator = ';')
                {
                    Separator = separator;
                    return Create();
                }
            }
        }
        
        
      2. PostcodeJson
        using System;
        using System.Collections.Generic;
        using System.IO;
        
        namespace PostcodeApp.Dal
        {
            class PostcodeJson : IPostcode
            {
                // Een Postcode BLL object om de opgehaalde waarden
                // in op te slagen
                public Bll.Postcode Postcode { get; set; }
                 // Error message
                public string Message { get; set; }
                private string connectionString = @"Data/Postcode";
                public string ConnectionString
                {
                    get
                    {
                        return connectionString + ".json";
                    }
                    set
                    {
                        connectionString = value;
                    }
                }
                public char Separator { get; set; } = ';';
                public PostcodeJson(Bll.Postcode postcode)
                {
                    Postcode = postcode;
                }
        
                // een overload om de naam van het csv bestand in te stellen
                public PostcodeJson(string connectionString)
                {
                    ConnectionString = connectionString;
                }
                /// <summary>
                /// In het geval van JSON wordt heel de List gesaved
                /// </summary>
                public bool Create()
                {
                    try
                    {
                        TextWriter writer = new StreamWriter(ConnectionString);
                        // static method SerilizeObject van Newtonsoft.Json
                        string postcodeString = Newtonsoft.Json.JsonConvert.SerializeObject(Postcode.List);
                        writer.WriteLine(postcodeString);
                        writer.Close();
                        Message = $"Het bestand met de naam {ConnectionString} is met succes geserialiseerd.";
                        return true;
                    }
                    catch (Exception e)
                    {
                        // Melding aan de gebruiker dat iets verkeerd gelopen is.
                        // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                        Message = $"Kan het bestand met de naam {ConnectionString} niet serialiseren.\nFoutmelding {e.Message}.";
                        return false;
                    }
                }
                public bool Create(char separator = ';')
                {
                    Separator = separator;
                    return Create();
                }
        
                public bool ReadAll()
                {
                    try
                    {
                        Helpers.Tekstbestand bestand = new Helpers.Tekstbestand();
                        bestand.FileName = ConnectionString;
                        bestand.Lees();
                        Postcode.List = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Bll.Postcode>>(bestand.Text);
                        Message = $"Het bestand met de naam {ConnectionString} is met succes geserialiseerd.";
                        return true;
                    }
                    catch (Exception e)
                    {
                        // Melding aan de gebruiker dat iets verkeerd gelopen is.
                        // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                        Message = $"Kan het bestand met de naam {ConnectionString} niet deserialiseren.\nFoutmelding {e.Message}.";
                        return false;
                    }
                }
            }
        }
        
        
      3. PostcodeXml
        using System;
        using System.Collections.Generic;
        using System.IO;
        using System.Xml.Serialization;
        
        namespace PostcodeApp.Dal
        {
            class PostcodeXml : IPostcode
            {
                // Een Postcode BLL object om de opgehaalde waarden
                // in op te slagen
                public Bll.Postcode Postcode { get; set; }
                // Error message
                public string Message { get; set; }
                private string connectionString = @"Data/Postcode";
                public string ConnectionString
                {
                    get
                    {
                        return connectionString + ".xml";
                    }
                    set
                    {
                        connectionString = value;
                    }
                }
                public char Separator { get; set; } = ';';
        
                public PostcodeXml()
                {
        
                }
        
                public PostcodeXml(Bll.Postcode postcode)
                {
                    Postcode = postcode;
                }
        
                // een overload om de naam van het csv bestand in te stellen
                public PostcodeXml(string connectionString)
                {
                    ConnectionString = connectionString;
                }
        
                /// <summary>
                /// In het geval van XML wordt heel de List gesaved.
                /// </summary>
                /// <returns></returns>
                public bool Create()
                {
                    try
                    {
                        XmlSerializer serializer = new XmlSerializer(typeof(Bll.Postcode[]));
                        TextWriter writer = new StreamWriter(ConnectionString);
                        //De serializer werkt niet voor een generieke lijst en ook niet voor ArrayList
                        // dus eerst omzetten naar array
                        Bll.Postcode[] postcodes = Postcode.List.ToArray();
                        serializer.Serialize(writer, postcodes);
                        writer.Close();
                        Message = $"Bestand {ConnectionString} is met succes geserialiseerd.";
                        return true;
                    }
                    catch (Exception e)
                    {
                        // Melding aan de gebruiker dat iets verkeerd gelopen is.
                        // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                        Message = $"Bestand {ConnectionString} is niet geserialiseerd.\nFoutmelding {e.Message}.";
                        return false;
                    }
                }
        
                public bool Create(char separator = ';')
                {
                    Separator = separator;
                    return Create();
                }
                public bool ReadAll()
                {
                    try
                    {
                        XmlSerializer serializer = new XmlSerializer(typeof(Bll.Postcode[]));
                        StreamReader file = new System.IO.StreamReader(ConnectionString);
                        Bll.Postcode[] postcodes = (Bll.Postcode[])serializer.Deserialize(file);
                        file.Close();
                        // array converteren naar List
                        Postcode.List = new List<Bll.Postcode>(postcodes);
                        Message = $"Bestand {ConnectionString} is met succes gedeserialiseerd.";
                        return true;
                    }
                    catch (Exception e)
                    {
                        // Melding aan de gebruiker dat iets verkeerd gelopen is.
                        // We gebruiken hier de nieuwe mogelijkheid van C# 6: string interpolatie
                        Message = $"Het bestand {ConnectionString} s niet gedeserialiseerd.\nFoutmelding {e.Message}.";
                        return false;
                    }
        
                }
            }
        }
        
        
  4. Registratie van een service

    1. Nu moeten we onze dependency injection container installeren. Pas dan beschikken we over een manier om de afzonderlijke componenten te registreren die we in ons programma zullen gebruiken.

      Deze wordt geleverd door de klasse ServiceCollection. We maken een instantie van de ServiceCollection-klasse en voegen services toe aan deze klasse.

      Voordat we kunnen praten over hoe injectie in de praktijk wordt gedaan, is het cruciaal om te begrijpen wat de levensduur van de service is. Wanneer een component een andere component inroept ​​via dependency inejction kan je bepalen of de instantie die de vragende component terugkrijgt uniek is voor die compenent of niet, m.a.w. je kan de levensduur van de opgevraagde component bepalen. Het levensduur bepaalt dus hoe vaak een component wordt geInstantieerd, en of een component wordt gedeeld of niet.

    2. De ingebouwde DI-container in .NET Core heeft drie opties:

      Applicatie-brede configuratiecontainers registreer je als Singleton. Database toegangsklassen zoals DbContext van Entity Framework registreer je best als Scoped, zodat de verbinding opnieuw kan worden gebruikt. Als je iets parallel wilt uitvoeren is het beter om dat te registreren als Transient. Dan krijgt elke component zijn eigen instantie en kunnen zij parallel lopen.

      Ik registreer het bestandstype als een concrete implementatie van de IPostcode-interface met behulp van een singleton-scope. Door dit te doen, probeer ik de program klasse te gebruiken als een shell die de servicecontainer maakt en de benodigde services registreert.

      1. Singleton: betekent dat er maar een enkele instantie ooit zal worden gemaakt. Deze instantie wordt gedeeld tussen alle componenten die het nodig hebben. Het is dezelfde instantie die altijd opnieuw wordt gebruikt.
      2. Scoped: betekent er per scope één instantie wordt gemaakt. Er wordt een scope bij elke request aan de applicatie gecreëerd, waardoor alle componenten die geregistreerd zijn als Scoped, één keer per request worden gecreëerd.
      3. Transient (vergankelijk) componenten worden gemaakt telkens ze worden aangevraagd en ze worden nooit gedeeld.
    3. Registratie van de Postcode service-provider
      We maken een methode in de Program klasse met de naam ConfigureServices():
      private static void ConfigureServices(IServiceCollection serviceCollection)
      {
              serviceCollection.AddSingleton<PostcodeApp.Dal.IPostcode>
                  (p => new PostcodeApp.Dal.PostcodeJson(new PostcodeApp.Bll.Postcode()));
      }
  5. Gebruik maken van een service
    1. we roepen deze methode op in de Main van de Program klasse;
    2. we configureren de services;
    3. we builden de ServiceProvider;
    4. we halen de service-provider op die we nodig hebben;
    5. en tenslote voeren we de generieke TryOut methode uit;
      static void Main(string[] args)
      {
          Console.WriteLine("Hello World!");
          ServiceCollection serviceCollection = new ServiceCollection();
          ConfigureServices(serviceCollection);
      
          ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
              var service = serviceProvider.GetService<IPostcode>();
          TryOut(service);
      }
    6. De TryOut methode
      static void TryOut(Dal.IPostcode dal)
      {
          Console.WriteLine("De Postcode App");
          // de seperator staat standaard op ;
          // in het Postcode.csv bestand is dat |
          dal.Separator = ';';
          dal.ReadAll();
          Console.WriteLine(dal.Message);
          View.PostcodeConsole view = new View.PostcodeConsole(dal.Postcode);
          view.List();
          // serialize postcodes met een andere separator
          // naar ander bestand
          dal.ConnectionString = "Data/Postcode2";
          dal.Create(';');
          Console.WriteLine(dal.Message);
      }
    7. Als we beslissen om een andere provider te gebruiken volstaat het die in de dependency-container te injecteren:
      private static void ConfigureServices(IServiceCollection serviceCollection)
      {
          // add services
          serviceCollection.AddSingleton<Dal.IPostcode>(p => new Dal.PostcodeJson(new Bll.Postcode()));
          // add app
          serviceCollection.AddTransient<App>();
      }

      Zoals je kan zien is deze manier van werken eenvoudiger dan de verschillende TryOut methoden uit Realisatiefase PostcodeApp. We hebben nu slechts één tryout methode die we netjes in een App klasse hebben ondergebracht.

  6. Om de scheiding (seperation of concern) tussen onze bedrijfsgebaseerde logica en de logica die we gebruiken om de werkelijke console-applicatie te configureren te behouden, maken we een nieuwe klasse met de naam App.cs. Het is de bedoeling om Program.cs te gebruiken om alles wat onze app nodig heeft op te starten, zoals de services die nodig zijn voor onze business logica.
    1. De eigenlijke businesslogica, die nu staat in de methode TryOut van de Program klasse verplaatsen we naar App.cs.
      using System;
      
      namespace PostcodeApp
      {
          public class App
          {
              private readonly Dal.IPostcode dal;
      
              public App(Dal.IPostcode postcodeDal)
              {
                  this.dal = postcodeDal;
              }
      
              public void TryOut()
              {
                  Console.WriteLine("De Postcode App");
                  // de seperator staat standaard op ;
                  // in het Postcode.csv bestand is dat |
                  dal.Separator = ';';
                  dal.ReadAll();
                  Console.WriteLine(dal.Message);
                  View.PostcodeConsole view = new View.PostcodeConsole(dal.Postcode);
                  view.List();
                  // serialize postcodes met een andere separator
                  // naar ander bestand
                  dal.ConnectionString = "Data/Postcode2";
                  dal.Create(';');
                  Console.WriteLine(dal.Message);
              }
          }
      }
      
      
    2. App.cs heeft een object nodig dat voldoet aan het Dal/IPostcode-interfacecontract. Dit object wordt door onze dependency manager doorgegeven. Open het Program.cs bestand en voeg het volgende toe:
      using Microsoft.Extensions.DependencyInjection;
      using PostcodeApp.Dal;
      using System;
      
      namespace PostcodeApp
      {
          class Program
          {
              static void Main(string[] args)
              {
                  Console.WriteLine("Hello World!");
                  ServiceCollection serviceCollection = new ServiceCollection();
                  ConfigureServices(serviceCollection);
      
                  ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
                  serviceProvider.GetService<App>().TryOut();
              }
      
              private static void ConfigureServices(IServiceCollection serviceCollection)
              {
                  serviceCollection.AddSingleton<PostcodeApp.Dal.IPostcode>
                      (p => new PostcodeApp.Dal.PostcodeJson(new PostcodeApp.Bll.Postcode()));
                  // add app
                  serviceCollection.AddTransient<App>();
              }
          }
      }
      Onze console app start in Main. Hier creëren we een nieuw ServiceCollection-object en configureren het in de ConfigureServices methode. In ConfigureServices voegen we onze dependancies toe aan de containercollectie. Die kunnen een levensduur van Scoped, Transient of Singleton hebben.

      Zodra het object ServiceCollection is geconfigureerd, moeten we een IServiceProvider (Dependency Management Container) opvragen uit ons ServiceCollection-object waarmee we handmatig onze App-klasse handmatig instantiëren die de businesslogica van onze app uitvoert door middel van de methode Run.

JI

2020-10-20 16:19:17